package com.nulldev.util.web.nTubeExtensions.services.Deezer;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.atomic.LongAdder;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.Arguments.ArgumentInit;
import com.nulldev.util.JVM.Arguments.ParsedArguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.JVM.arguments.ArgumentWithValue;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.ByteUtil;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Charsets.CharsetUtil;
import com.nulldev.util.data.FastMath.FastMath;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.io.file.FileUtil;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.HttpClient.contentGen.HttpFormGenerator;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.URLUtils;
import com.nulldev.util.web.legacyURI.LegacyURIUtils;
import com.nulldev.util.web.nTubeExtensions.services.Deezer.Undeezerified.Lyrics.Lyric;

public class Undeezerified {
	/*
	 * After ${window.Infinity} years in development, it'll probably break in the
	 * future. Lessons learnt: - Whoever thought of ISO charsets should have their
	 * neck ripped out and thrown away. - xor() is a bitch. TODO: Completed: - Nuke
	 * MultipartUtility - Cover art and metadata
	 * 
	 * Changelog: 1.0.2 - Moved to HttpFormGenerator over MultipartUtil. - Fixed
	 * missing MD5_ORIGIN. WARNING: The fallback method uses whatever quality your
	 * account allows for. For free accounts, this is MP3 @ 128kbps. 1.0.1 - Removed
	 * asObject() references. 1.0.0 - Public and stable release.
	 */

	public static final String VERSION = "1.0.2";

	public class TrackInfo {
		public final String MD5_ORIGIN;
		public final long SNG_ID;
		public final String MEDIA_VERSION;

		public TrackInfo(final String md5, final long id, final String version) {
			this.MD5_ORIGIN = md5;
			this.SNG_ID = id;
			this.MEDIA_VERSION = version;
		}
	}

	private static final Logger log = LoggerFactory.getLoggerD(true);

	private static final boolean USE_GLOBAL_AGENT = Arguments.notArguments("--Undeezerified.useLegacyUA");
	private static final Entry[] DEFAULT_ENTRIES = new Entry[]
		{ new Entry("User-Agent",
				USE_GLOBAL_AGENT ? URLUtils.STANDARD_USERAGENT
						: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"),
				new Entry("Cache-Control", "max-age=0"), new Entry("Accept-Language", "en-US,en;q=0.9,en-US;q=0.8,en;q=0.7"),
				new Entry("Accept-Charset", "utf-8,ISO-8859-1;q=0.8,*;q=0.7"), new Entry("Content-Type", "text/plain;charset=UTF-8"), };

	public static final Entry[] QUERIES = new Entry[]
		{ new Entry("api_version", "1.0"), new Entry("api_token", ""), new Entry("input", "3") };

	private volatile List<String> loginCookies = null;
	private volatile boolean loggedIn = false;
	private volatile UniJsonValue dataObject = null;
	private volatile String formToken = null;
	private volatile String licenseToken = null;

	@SuppressWarnings("static-access")
	private static double getAPI_CID() {
		return Math.floor(0x3b9aca00 * FastMath.DEFAULT_INSTANCE.random());
	}

	public Undeezerified() {
	}

	@Deprecated
	public boolean init(final String username, final String password) throws Exception {
		String qs = "?";
		qs += "api_version=1.0&";
		qs += "api_token=&";
		qs += "input=3&";
		qs += "method=deezer.getUserData&";
		qs += "cid=" + getAPI_CID();
		final HttpResponse<String> r0 = HttpClient.getString().url("https://www.deezer.com/ajax/gw-light.php" + qs).headers(DEFAULT_ENTRIES).create();
		final String[] cookies = r0.cookies();
		if (r0.status() == 411) {
			log.error("init() -> Unknown request sent. ???");
			return false;
		}
		final UniJsonValue obj0 = UniJsonValue.parse(r0.dataThenClose());
		if (!obj0.get("error").asArray().isEmpty()) {
			log.error("init() -> State check failed!", obj0.get("error"));
			return false;
		}
		String cookiePayload = "";
		for (final String cookie : cookies) {
			cookiePayload += cookie.split(";")[0] + ";";
		}
		log.info("init() -> Logging in...");
		try {
			log.info("loginCheck: " + obj0.get("results").getString("checkFormLogin"));
//			final MultipartUtility multi = new MultipartUtility("https://www.deezer.com/ajax/action.php", "UTF-8", true, "POST",
//					ArrayUtils.add(DEFAULT_ENTRIES, new Entry("Cookie", cookiePayload)));
//			multi.addFormField("type", "login");
//			multi.addFormField("mail", LegacyURIUtils.safeEncode(username));
//			multi.addFormField("password", password);
//			multi.addFormField("checkFormLogin", obj0.get("results").asObject().getString("checkFormLogin"));
			final HttpResponse<String> s = HttpClient.getString().url("https://www.deezer.com/ajax/action.php")
					.headers(ArrayUtils.add(DEFAULT_ENTRIES, new Entry("Cookie", cookiePayload)))
					.create(HttpMethod.POST, new HttpFormGenerator().add("type", "login").add("mail", LegacyURIUtils.safeEncode(username))
							.add("password", password).add("checkFormLogin", obj0.get("results").getString("checkFormLogin")));
			if (s.successful()) {
				IOUtils.closeQuietly(s);
				throw new RuntimeException("Login failed: Response code: " + s.status());
			}
			final String resp = new String(s.dataThenClose());
			if (resp.equalsIgnoreCase("success")) {
				this.loginCookies = s.cookiesAsList();
				this.loggedIn = true;
				log.info("init() -> Logged in!");
				log.info("init() -> Downloading metadata...");
				cookiePayload = "";
				for (final String cookie : this.loginCookies) {
					cookiePayload += cookie.split(";")[0] + ";";
				}
				final String data = HttpClient.getString()
						.url("https://www.deezer.com/ajax/gw-light.php" + "?api_version=1.0&api_token=&input=3&method=deezer.getUserData&cid=" + getAPI_CID())
						.headers(DEFAULT_ENTRIES).header(new Entry("Cookie", cookiePayload)).create().dataThenClose();
				this.dataObject = UniJsonValue.parse(data);
				this.formToken = this.dataObject.get("results").getString("checkForm");
				this.loggedIn = true;
				log.info("init() -> Metadata processed.");
				return true;
			} else {
				/*
				 * Reasons: - Invalid email/password - FUCKING RECAPTCHA
				 */
				log.error("init() -> Failed to login. (expected 'success', got: " + resp + ")");
				return false;
			}
		} catch (Exception e) {
			log.error("init() -> Failed to login. (hit an exception)", e);
			return false;
		}
	}

	public boolean init(final String arl_token) throws Exception {
		try {
			String qs = "?";
			qs += "api_version=1.0&";
			qs += "api_token=&";
			qs += "input=3&";
			qs += "method=deezer.getUserData&";
			qs += "cid=" + getAPI_CID();
			final HttpResponse<String> r0 = HttpClient.getString().url("https://www.deezer.com/ajax/gw-light.php" + qs).headers(DEFAULT_ENTRIES)
					.header(new Entry("Cookie", "arl=" + arl_token)).create();
			this.dataObject = UniJsonValue.parse(r0.dataThenClose());
			if (log.debugEnabled.get())
				this.dataObject.println();
			if (this.dataObject.contains("results") && this.dataObject.get("results").contains("USER")
					&& this.dataObject.mget("results.USER").contains("OPTIONS") && this.dataObject.mget("results.USER.OPTIONS").contains("license_token")) {
				this.licenseToken = this.dataObject.mget("results.USER.OPTIONS").getString("license_token");
			}
			this.formToken = this.dataObject.get("results").getString("checkForm");
			this.loginCookies = new ArrayList<String>();
			this.loginCookies.addAll(Arrays.asList(r0.cookies()));
			this.loginCookies.add("arl=" + arl_token + ";");
			this.loggedIn = true;
			log.trace("init() -> Metadata processed.");
			return true;
		} catch (Exception e) {
			log.error("init() -> Failed to login. (hit an exception, invalid token?)", e);
			return false;
		}
	}

	public static class SearchResults {

	}

	public static class SearchResult {
		public final long trackId;
		public final String trackName;
		public final String artist;

		public SearchResult(final long trackId, final String trackName, final String artist) {
			this.trackId = trackId;
			this.trackName = trackName;
			this.artist = artist;
		}
	}

	public static void search(final String query) throws Exception {
		if (query == null || query.isEmpty())
			return;
		final String l = "https://api.deezer.com/search?q=" + LegacyURIUtils.safeEncode(query);
		final HttpResponse<String> s = HttpClient.getString().url(l).create();
		final String response = s.dataThenClose();
		final UniJsonValue v = UniJsonValue.parse(response);
		final UniJsonValue r = v.get("data").asArray();
		// TODO
	}

	private static final String SECONDARY_KEY = (new Object() {
		int t;

		public String toString() {
			byte[] buf = new byte[16];
			t = -1503041676;
			buf[0] = (byte) (t >>> 8);
			t = -213183510;
			buf[1] = (byte) (t >>> 20);
			t = 1824314365;
			buf[2] = (byte) (t >>> 21);
			t = -1744606091;
			buf[3] = (byte) (t >>> 8);
			t = -204039449;
			buf[4] = (byte) (t >>> 7);
			t = 116637808;
			buf[5] = (byte) (t >>> 1);
			t = -102817601;
			buf[6] = (byte) (t >>> 18);
			t = 1624828300;
			buf[7] = (byte) (t >>> 14);
			t = 1387465704;
			buf[8] = (byte) (t >>> 12);
			t = 918363661;
			buf[9] = (byte) (t >>> 15);
			t = -1109680997;
			buf[10] = (byte) (t >>> 18);
			t = 593074276;
			buf[11] = (byte) (t >>> 14);
			t = -1435384017;
			buf[12] = (byte) (t >>> 5);
			t = 997243549;
			buf[13] = (byte) (t >>> 19);
			t = 1277651098;
			buf[14] = (byte) (t >>> 21);
			t = 1770545915;
			buf[15] = (byte) (t >>> 19);
			return new String(buf);
		}
	}.toString());

	private static byte[] getBlowfishKey(long songId) {
		if (songId < 0) {
			songId = songId * -1;
		}
		String hash = md5(String.valueOf(songId));
		String part1 = hash.substring(0, 16);
		String part2 = hash.substring(16, 32);
		String[] data = new String[]
			{ SECONDARY_KEY, part1, part2 };
		String keyStr = getXor(data, 16);
		return keyStr.getBytes();
	}

	private static String getXor(String[] data, int len) {
		String result = "";
		for (int i = 0; i < len; ++i) {
			int character = data[0].charAt(i);
			for (int j = 1; j < data.length; ++j) {
				character ^= data[j].charAt(i);
			}
			result = result + (char) character;
		}
		return result;
	}

	private static byte[] decryptBlowfish(final byte[] data, final byte[] key) {
		try {
			final SecretKeySpec keySpec = new SecretKeySpec(key, "Blowfish");
			final Cipher cipher = Cipher.getInstance("Blowfish/CBC/NoPadding");
			cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(new byte[]
				{ 0, 1, 2, 3, 4, 5, 6, 7 }));
			return cipher.doFinal(data);
		} catch (Exception e) {
			JVM.errorln(e);
			return data;
		}
	}

	@SuppressWarnings("unused")
	private static String bytesToHexString(byte[] bytes) {
		StringBuilder result = new StringBuilder(bytes.length * 2);
		for (byte b : bytes) {
			String hex = Integer.toHexString(b & 255);
			if (hex.length() == 1) {
				result.append('0');
			}
			result.append(hex);
		}
		return result.toString();
	}

	public static class Lyrics {
		public static class Lyric {
			public final String text;
			public final long millis;

			public Lyric(final String text, final long millis) {
				this.text = text;
				this.millis = millis;
			}
		}

		public final List<Lyric> list;

		public Lyrics() {
			this.list = FastLists.list(64);
		}

		protected void add(final Lyric l) {
			this.list.add(l);
		}
	}

	public static class Song {
		public final long songID;
		public final InputStream stream;
		public final String trackName;
		public final String artist;
		public final String albumName;
		public final String albumPicture;
		public final String albumId;
		public final long length;
		public Lyrics lyrics;

		public Song(final long songID, final InputStream is, final String trackName, final String artist, final String albumName, final String albumPicture,
				final String albumId, final long length) {
			this.songID = songID;
			this.stream = is;
			this.trackName = trackName;
			this.artist = artist;
			this.albumName = albumName;
			this.albumPicture = albumPicture;
			this.albumId = albumId;
			this.length = length;
		}

		public boolean hasLyrics() {
			return this.lyrics != null;
		}

		protected Song setLyrics(final Lyrics l) {
			this.lyrics = l;
			return this;
		}

		public static enum ImageSize {
			SMALL("small"),
			MEDIUM("medium"),
			LARGE("large"),
			VERY_LARGE("xl"),
			NATIVE("native")

			;

			public final String id;

			private ImageSize(final String id) {
				this.id = id;
			}
		}

		public String getImageURL(final ImageSize size) {
			switch (size) {
				case NATIVE:
					return "https://api.deezer.com/album/" + albumId + "/image";
				case VERY_LARGE:
					return "https://e-cdns-images.dzcdn.net/images/cover/" + albumPicture + "/1000x1000-000000-80-0-0.jpg";
				case LARGE:
					return "https://e-cdns-images.dzcdn.net/images/cover/" + albumPicture + "/500x500-000000-80-0-0.jpg";
				case MEDIUM:
					return "https://e-cdns-images.dzcdn.net/images/cover/" + albumPicture + "/250x250-000000-80-0-0.jpg";
				case SMALL:
				default:
					return "https://e-cdns-images.dzcdn.net/images/cover/" + albumPicture + "/56x56-000000-80-0-0.jpg";
			}
		}
	}

	public Song getSong(final String songURL, final AudioQuality audioQuality) throws Exception {
		if (!this.loggedIn) {
			throw new UnsupportedOperationException("getSong() -> Not logged in!");
		}
		if (audioQuality == null) {
			throw new NullPointerException("getSong() -> audioQuality == null!");
		}
		long songID = MathUtil.safeParseL(songURL, -1);
		if (songID == -1 && songURL.startsWith("https://www.deezer.com/")) {
			songID = MathUtil.safeParseL(ArrayUtils.last(songURL.split("/")), -1);
		}
		if (songID == -1) {
			throw new UnsupportedOperationException("getSong() -> Invalid song ID! [got: " + songURL + "]");
		}
		final int _songID = (int) songID;
		log.trace("getSong() -> Downloading song ID '" + songID + "'...");
		final UniJsonValue trackInfo = getTrackInfo(songID);
		if (log.debugEnabled.get())
			trackInfo.println();
		String md5Origin = trackInfo.get("results").getSafe("DATA").getString("MD5_ORIGIN");
		if (md5Origin == null) {
			log.warn("MD5 origin is missing, falling back to account-enforced quality method.");
			log.debug("MD5(TRACK_TOKEN): "
					+ (trackInfo.get("results").getSafe("DATA").contains("TRACK_TOKEN") ? md5(trackInfo.get("results").getSafe("DATA").getString("TRACK_TOKEN"))
							: "N/A"));
		}
		log.trace("getSong() -> MD5 origin: " + md5Origin);
		final TrackInfo ti = new TrackInfo(md5Origin, songID, trackInfo.get("results").getSafe("DATA").getString("MEDIA_VERSION"));
		final boolean USE_NEW_API = (md5Origin == null);
		String trackURL = null;
		if (USE_NEW_API) {
			final UniJsonValue payloadObj = trackInfo.contains("FALLBACK") ? trackInfo.mget("results.DATA.FALLBACK") : trackInfo.mget("results.DATA");
			final String payload = generateMediaFetchPayload(payloadObj, this.licenseToken);
			log.info("payload: " + payload);
			final String r = HttpClient.getString().url("https://media.deezer.com/v1/get_url").create(HttpMethod.POST, payload).dataThenClose();
			final UniJsonValue s = UniJsonValue.parse(r);
			if (!s.contains("data") || s.get("data").length() <= 0)
				throw new RuntimeException("Failed to get track URLs using new API!");
			final UniJsonValue data = s.get("data");
			if (data == null || data.isNull())
				throw new NullPointerException("data is null!");
			if (log.debugEnabled.get())
				data.println();
			final UniJsonValue payloadResponse = data.first().get("media").first();
			log.debug("Track Cipher: " + payloadResponse.get("cipher").getString("type", "N/A"));
			String c = null;
			for (final UniJsonValue e : payloadResponse.get("sources").asArray()) {
				if (e.getString("provider", "").equalsIgnoreCase("EC")) {
					c = e.getString("url");
					break;
				}
			}
			if (c == null)
				throw new RuntimeException("Couldn't find a track URL with type EC!");
			trackURL = c;
			if (ATTEMPT_MD5_CRACKING) {
				log.warn("Attempting to crack MD5, this won't end well.");
				final String secureHash0 = StringsUtil.substringBetweenO(trackURL, "/media/", "?");
				String secureHash = secureHash0.substring(2);
				log.info("secureHash: " + secureHash);
				while (secureHash.length() % 16 > 0)
					secureHash += ' ';
				log.info("Running reverse AES on our URL...");
				final String rawHash = unAES(secureHash);
				log.info("rawHASH: " + rawHash);
			}
		} else
			trackURL = getTrackURL(ti, audioQuality.id());
		log.trace("getSong() -> Track URL: " + trackURL);
		final HttpResponse<InputStream> data = HttpClient.getStream().url(trackURL).headers(DEFAULT_ENTRIES).create(HttpMethod.GET); // FIXME: use
																																		// enableBuffering()
		log.trace("getSong() -> Downloading... (response code: " + data.status() + ")");
		if (data.status() == 403 && audioQuality != AudioQuality.LOW_MPEG3) {
			log.warn("getSong() -> Got 404/403 for quality: " + audioQuality);
			return getSong(songURL, AudioQuality.LOW_MPEG3);
		}
		if (data.status() != 200) {
			log.error("getSong() -> Failed to get song. [got: " + data.status() + "]");
			return null;
		}
		final BufferedInputStream bis = new BufferedInputStream(data.data(), IOUtils.MEMORY_ALLOC_BUFFER);
		// FIXME: Check if we can get away with this instead of 8192.
		final byte[] key = getBlowfishKey(_songID);
		log.trace("Key: " + new String(key));
		String artists = null;
		for (final UniJsonValue o : trackInfo.get("results").get("DATA").get("ARTISTS").asArray()) {
			if (artists == null) {
				artists = o.getString("ART_NAME");
			} else {
				artists += ", " + o.getString("ART_NAME");
			}
		}
		Lyrics l = null;
		if (trackInfo.get("results").hasValue("LYRICS")) {
			if (trackInfo.get("results").get("LYRICS").hasValue("LYRICS_SYNC_JSON")) {
				l = new Lyrics();
				final Lyrics _l = l;
				trackInfo.get("results").get("LYRICS").get("LYRICS_SYNC_JSON").asArray().forEach((lyric) -> {
					if (!lyric.hasValue("milliseconds"))
						return;
					_l.add(new Lyric(lyric.getString("line"), Long.parseLong(lyric.getString("milliseconds"))));
				});
			}
		}
		return new Song(songID, new InputStream() {

//			int chunk = 0;
			final LongAdder la = new LongAdder();
			final int chunkSize = 2048;

			@Override
			public int read() throws IOException {
				throw new IOException("Single byte reads not supported!");
			}

			@Override
			public int read(byte[] b, int off, int len) throws IOException {
				byte[] buf = new byte[chunkSize];
				final int r = IOUtils.readFully(bis, buf, 0, buf.length); // bis.read(buf, 0, buf.length);
				log.trace("r: " + r + ", cc: " + la.sum() + ", dep: " + (la.intValue() % 3 == 0));
				if (r == 0)
					return -1;
				if (la.intValue() % 3 == 0 && r == chunkSize) {
					buf = decryptBlowfish(buf, key);
				}
				System.arraycopy(buf, off, b, off, r);
				la.increment();
				return r;
			}

			@Override
			public void close() throws IOException {
				bis.close();
				super.close();
			}
		}, trackInfo.get("results").get("DATA").getString("SNG_TITLE", null), artists, trackInfo.get("results").get("DATA").getString("ALB_TITLE", null),
				trackInfo.get("results").get("DATA").getString("ALB_PICTURE", null), trackInfo.get("results").get("DATA").getString("ALB_ID", null),
				data.length()).setLyrics(l);
	}

	public InputStream streamSong(final String songURL, final AudioQuality audioQuality) throws Exception {
		if (!this.loggedIn) {
			throw new UnsupportedOperationException("downloadSong() -> Not logged in!");
		}
		if (audioQuality == null) {
			throw new NullPointerException("downloadSong() -> audioQuality == null!");
		}
		long songID = -1;
		if (songID == -1 && songURL.startsWith("https://www.deezer.com/")) {
			songID = MathUtil.safeParseL(ArrayUtils.last(songURL.split("/")), -1);
		}
		if (songID == -1) {
			throw new UnsupportedOperationException("downloadSong() -> Invalid song ID!");
		}
		final int _songID = (int) songID;
		log.trace("downloadSong() -> Downloading song ID '" + songID + "'...");
		final UniJsonValue trackInfo = getTrackInfo(songID);
		final String md5Origin = trackInfo.get("results").get("DATA").getString("MD5_ORIGIN");
		log.trace("downloadSong() -> MD5 origin: " + md5Origin);
		final TrackInfo ti = new TrackInfo(md5Origin, songID, trackInfo.get("results").get("DATA").getString("MEDIA_VERSION"));
		final String trackURL = getTrackURL(ti, audioQuality.id());
		log.trace("downloadSong() -> Track URL: " + trackURL);
		final HttpResponse<InputStream> data = HttpClient.getStream().url(trackURL).headers(DEFAULT_ENTRIES).create(HttpMethod.GET);
		log.trace("downloadSong() -> Downloading... (response code: " + data.status() + ")");
		if (data.status() != 200) {
			log.error("downloadSong() -> Failed to generate correct song reference. [got: " + data.status() + "]");
			return null;
		}
		final BufferedInputStream bis = new BufferedInputStream(data.data(), IOUtils.MEMORY_ALLOC_BUFFER);
		// TODO: Check if we can get away with this instead of 8192.
		final byte[] key = getBlowfishKey(_songID);
		log.trace("Key: " + new String(key));
		return new InputStream() {

			int chunk = 0;
			final int chunkSize = 2048;

			@Override
			public int read() throws IOException {
				throw new IOException("Single byte reads not supported!");
			}

			@Override
			public int read(byte[] b, int off, int len) throws IOException {
				byte[] buf = new byte[chunkSize];
				final int r = IOUtils.readFully(bis, buf, 0, buf.length); // bis.read(buf, 0, buf.length);
				log.trace("r: " + r + ", cc: " + chunk + ", dep: " + (chunk % 3 == 0));
				if (r == 0)
					return -1;
				if (chunk % 3 == 0 && r == chunkSize) {
					buf = decryptBlowfish(buf, key);
				}
				System.arraycopy(buf, off, b, off, r);
				chunk += 1;
				return r;
			}

			@Override
			public void close() throws IOException {
				bis.close();
				super.close();
			}
		};
	}

	private static final boolean USE_ALT_TRACKINFO_METHOD = false;
	private static final boolean ATTEMPT_MD5_CRACKING = false;

	private UniJsonValue getTrackInfo(final long trackID) throws Exception {
		if (!this.loggedIn) {
			throw new UnsupportedOperationException("downloadSong() -> Not logged in!");
		}
		if (trackID == -1) {
			throw new UnsupportedOperationException("downloadSong() -> Invalid song ID!");
		}
		String qs = "?";
		String method = "deezer.pageTrack";
		if (trackID < 0 || USE_ALT_TRACKINFO_METHOD) {
			method = "song.getData";
		}
		qs += "api_version=1.0&";
		qs += "api_token=" + LegacyURIUtils.safeEncode(this.formToken) + "&";
		qs += "input=3&";
		qs += "method=" + method + "&";
		qs += "cid=" + getAPI_CID();
		String cookiePayload = "";
		for (final String cookie : this.loginCookies) {
			cookiePayload += cookie.split(";")[0] + ";";
		}
		final HttpResponse<String> r0 = HttpClient.getString().url("https://www.deezer.com/ajax/gw-light.php" + qs).headers(DEFAULT_ENTRIES)
				.header(new Entry("Cookie", cookiePayload)).create(HttpMethod.POST, "{\"SNG_ID\": " + trackID + "}");
		return UniJsonValue.parse(r0.dataThenClose());
	}

	private static String md5(final String s) {
		try {
			final MessageDigest md = MessageDigest.getInstance("MD5");
			return ByteUtil.getHex(md.digest(s.getBytes(CharsetUtil.ISO_8859_1)));
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private static final String MAIN_CIPHER = (new Object() {
		int t;

		public String toString() {
			byte[] buf = new byte[16];
			t = 1781347551;
			buf[0] = (byte) (t >>> 24);
			t = 1863253568;
			buf[1] = (byte) (t >>> 24);
			t = 1625935272;
			buf[2] = (byte) (t >>> 6);
			t = -332132114;
			buf[3] = (byte) (t >>> 21);
			t = 1928721815;
			buf[4] = (byte) (t >>> 2);
			t = 487439516;
			buf[5] = (byte) (t >>> 7);
			t = -2048281275;
			buf[6] = (byte) (t >>> 11);
			t = 1908593004;
			buf[7] = (byte) (t >>> 9);
			t = -1215143455;
			buf[8] = (byte) (t >>> 8);
			t = 1789324591;
			buf[9] = (byte) (t >>> 5);
			t = -1333081934;
			buf[10] = (byte) (t >>> 9);
			t = 235293412;
			buf[11] = (byte) (t >>> 13);
			t = 1783910701;
			buf[12] = (byte) (t >>> 16);
			t = -656494186;
			buf[13] = (byte) (t >>> 2);
			t = -589917578;
			buf[14] = (byte) (t >>> 12);
			t = 1181223146;
			buf[15] = (byte) (t >>> 16);
			return new String(buf);
		}
	}.toString());

	private static String aes(final String clearText) {
		try {
			final SecretKeySpec skeySpec = new SecretKeySpec(MAIN_CIPHER.getBytes(CharsetUtil.ISO_8859_1), "AES");
			final Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
			cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
			final byte[] encrypted = cipher.doFinal(clearText.getBytes(CharsetUtil.ISO_8859_1));
			return ByteUtil.getHex(encrypted);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private static String unAES(final String clearText) {
		try {
			final SecretKeySpec skeySpec = new SecretKeySpec(MAIN_CIPHER.getBytes(CharsetUtil.ISO_8859_1), "AES");
			final Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
			cipher.init(Cipher.DECRYPT_MODE, skeySpec);
			final byte[] encrypted = cipher.doFinal(ByteUtil.fromHex(clearText));
			return new String(encrypted);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private static final char SEP = '\u00a4';

	/*
	 * Fun fact: This took 4 hours of actual effort to make it work. tl;dr Fuck you
	 * Windows and your shitty character encodings.
	 */
	private String getTrackURL(final TrackInfo trackInfos, final int trackQuality) throws Exception {
		if (trackInfos.MD5_ORIGIN == null)
			throw new NullPointerException("MD5_ORIGIN is null!");
		final String base = trackInfos.MD5_ORIGIN + SEP + trackQuality + SEP + trackInfos.SNG_ID + SEP + trackInfos.MEDIA_VERSION;
		log.trace("base: " + base);
		final String stage1 = md5(base); // WORKING
		log.trace("getTrackDownloadURL() -> Stage 1: " + stage1);
		String step2 = stage1 + SEP + base + SEP;
		while (step2.length() % 16 > 0)
			step2 += ' ';
		log.trace("getTrackDownloadURL() -> Stage 2: " + step2);
		final String hex = aes(step2).substring(0, 160);
		log.trace("getTrackDownloadURL() -> Stage 3: " + hex);
		return "https://e-cdns-proxy-" + trackInfos.MD5_ORIGIN.charAt(0) + ".dzcdn.net/mobile/1/" + hex;
	}

	private static final String[] NEW_TYPES = new String[]
		{ "PREVIEW", "FULL" };
	private static final String[] NEW_CIPHERS = new String[]
		{ "BF_CBC_STRIPE", "NONE" };
	private static final String[] NEW_FORMATS = new String[]
		{
				/* HiFi */

				"FLAC", "MP4_RA3", "MP4_RA2", "MP4_RA1",

				/* Meh-Tier */

				"SBC_256", "MP3_320", "MP3_256",

				/* Even YouTube Music provides better audio quality Tier */

				"MP3_192", "MP3_128", "MP3_64", "MP3_MISC", "AAC_96", "AAC_64" };

	private String generateMediaFetchPayload(final UniJsonValue trackDATA, final String licenseToken) {
		Variables.notNullE(trackDATA);
		Variables.notNullE(licenseToken);
		final UniJsonValue jv = UniJsonValue.object();
		jv.add("license_token", licenseToken);
		final UniJsonValue media = UniJsonValue.array();
		final UniJsonValue media_request = UniJsonValue.object();
		media_request.add("type", "FULL");
		final UniJsonValue media_formats = UniJsonValue.array();
		for (final String format : NEW_FORMATS) {
			final UniJsonValue f = UniJsonValue.object();
			f.add("format", format);
			f.add("cipher", "BF_CBC_STRIPE"); // FIXME: This will probably change?
			media_formats.add(f);
		}
		media_request.add("formats", media_formats);
		media.add(media_request);
		jv.add("media", media);
		final UniJsonValue track_tokens = UniJsonValue.array();
		if (trackDATA.contains("TRACK_TOKEN"))
			track_tokens.add(trackDATA.get("TRACK_TOKEN"));
		jv.add("track_tokens", track_tokens);
		return jv.toString();
	}

	public static void main(String[] args) throws Exception {
		Arguments.argumentInit(new ArgumentInit() {

			@Override
			public void onInit(Arguments args) {
				args.addArgument(new ArgumentWithValue.StringArgument("--arl", "Your Deezer API Key", "--arl", false));
			}
		});
		Arguments.setup(args);
		final ParsedArguments pa = Arguments.get();
		LegacyURIUtils.disableSSLVerification();
		final Undeezerified ud = new Undeezerified();
		final Scanner sc = new Scanner(System.in);
		final boolean noGoogle = false;
		if (noGoogle) {
			System.out.print("Email: ");
			final String email = sc.next();
			System.out.print("Password: ");
			final String password = sc.next();
			final boolean result = ud.init(email, password);
			JVM.println("init() -> Result: " + result);
//			if (result) {
//				JVM.println(ud.downloadSong("https://www.deezer.com/us/track/131329992", AudioQuality.LOW).length);
//			}
		} else {
			// System.out.print("ARL Token: ");
			final String arl = pa.getValue("--arl"); // sc.next();
			final boolean result = ud.init(arl);
			JVM.println("init() -> Result: " + result);
			if (result) {
				// FileUtil.writeFile(new File(FileUtil.HOME_DIRECTORY + "/sample.mp3"),
				// ud.downloadSong("https://www.deezer.com/us/track/131329992",
				// AudioQuality.LOW));
				// JVM.println("downloaded!");
				final boolean download = false;
				FileOutputStream fos = download ? new FileOutputStream(new File(FileUtil.HOME_DIRECTORY + "/sample.mp3")) : null;
				final String trackID = "1124841712";
				final Song i = ud.getSong("https://www.deezer.com/us/track/" + trackID, AudioQuality.LOW_MPEG3);
				if (!download) {
					i.stream.close();
				} else {
					IOUtils.copy(i.stream, fos);
					fos.flush();
					fos.close();
					JVM.println("downloaded!");
				}
			}
		}
		sc.close();
	}

}
